package ru.amse.stroganova.presentation;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Polygon;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import ru.amse.stroganova.graph.Edge;
import ru.amse.stroganova.graph.Vertex;

/**
 * This class provides a presentation for <tt>EdgeImpl</tt>.
 * 
 * @author Dasha Stroganova
 * 
 */
public class EdgePresentation {

	private final Edge edge;

	private final Map<Vertex, VertexPresentation> verticesMap;

	private Color color;

	private boolean isDirected;

	private boolean isWeighted;

	private static final int SELECTING_DISTANCE = 5;

	private static final int ARROW_WIGTH = 3;

	private static final int ARROW_LENGTH = 10;

	private static final double ARROW_SIDE = Math.sqrt(ARROW_WIGTH
			* ARROW_WIGTH + ARROW_LENGTH * ARROW_LENGTH);

	/**
	 * Weight Shift
	 */
	public static final int WEIGHT_SHIFT = 2;

	private int weightWidth;

	private int weightHeight;

	private final List<ElementStateListener> listeners;
	
	/**
	 * Creates edge presentation with given start and end points.
	 * 
	 * @param edge
	 *            edge
	 * @param verticesMap
	 *            map from vertices to their presentations
	 * @param isDirected
	 *            true if you want directed edge; false otherwise
	 * @param isWeighted
	 *            true if you want weighted edge; false otherwise
	 */
	public EdgePresentation(Edge edge,
			Map<Vertex, VertexPresentation> verticesMap, boolean isDirected,
			boolean isWeighted) {
		this.edge = edge;
		this.isWeighted = isWeighted;
		this.isDirected = isDirected;
		this.verticesMap = verticesMap;
		color = Color.black;
		listeners = new ArrayList<ElementStateListener>();
	}

	/**
	 * Creates new edge presentation and new edge.
	 * 
	 * @param sourcePres source vertex presentation
	 * @param destinPres source vertex presentation
	 * @param isDirected
	 *            true if you want directed edge; false otherwise
	 * @param isWeighted
	 *            true if you want weighted edge; false otherwise
	 */
	public EdgePresentation(VertexPresentation sourcePres, VertexPresentation destinPres, boolean isDirected,
			boolean isWeighted) {
		this.edge = new Edge(sourcePres.getVertex(), destinPres.getVertex());
		verticesMap = new HashMap<Vertex, VertexPresentation>();
		verticesMap.put(sourcePres.getVertex(), sourcePres);
		verticesMap.put(destinPres.getVertex(), destinPres);
		this.isWeighted = isWeighted;
		this.isDirected = isDirected;
		color = Color.black;
		listeners = new ArrayList<ElementStateListener>();
	}
	
	/**
	 * Adds the specified ElementStateListaner to recive notification about state
	 * change.
	 * 
	 * @param listener
	 */
	public void addElementStateListener(ElementStateListener listener) {
		listeners.add(listener);
	}
	
	/**
	 * Removes the specified ElementStateListaner from this presentation.
	 * 
	 * @param listener
	 */
	public void removeElementStateListener(ElementStateListener listener) {
		listeners.remove(listener);
	}

	private void fireStructureChanged() {
		for (ElementStateListener listener : listeners) {
			listener.elementStructureChanged();
		}
	}
	
	private Point calculatePoint(Point start, Point end) {
		double alpha = Math.atan((double) (start.y - end.y) / (start.x - end.x));
		if ((start.x - end.x) < 0) {
			alpha += Math.PI;
		}
		double headX = end.x + VertexPresentation.RADIX * Math.cos(alpha);
		double headY = end.y + VertexPresentation.RADIX * Math.sin(alpha);
		return new Point((int) Math.round(headX), (int) Math.round(headY));
	}

	/**
	 * Returns edge start point.
	 * 
	 * @return edge start point
	 */
	public Point getStart() {
		VertexPresentation sourcePresentation = verticesMap.get(edge
				.getSource());
		Point sourceCenter = sourcePresentation.getCenter();
		VertexPresentation destinationPresentation = verticesMap.get(edge
				.getDestination());
		Point destinationCenter = destinationPresentation.getCenter();
		return calculatePoint(destinationCenter, sourceCenter);
	}
	
	/**
	 * Returns edge end point.
	 * 
	 * @return edge end point
	 */
	public Point getEnd() {
		VertexPresentation sourcePresentation = verticesMap.get(edge
				.getSource());
		Point sourceCenter = sourcePresentation.getCenter();
		VertexPresentation destinationPresentation = verticesMap.get(edge
				.getDestination());
		Point destinationCenter = destinationPresentation.getCenter();
		return calculatePoint(sourceCenter, destinationCenter);
	}

	/**
	 * Returns model edge.
	 * 
	 * @return edge
	 */
	public Edge getEdge() {
		return edge;
	}

	/**
	 * Paints this edge using given graphics
	 * 
	 * @param gr
	 *            graphics
	 * @param backgroundColor
	 *            background color
	 */
	public void paint(Graphics gr, Color backgroundColor) {
		gr.setColor(color);
		Point start = getStart();
		Point end = getEnd();
		
		gr.drawLine(start.x, start.y, end.x, end.y);
		if (isDirected) {
			drawArrow(gr, start, end);
		}
		if (isWeighted) {
			drawWeight(gr, start, end, backgroundColor);
		}
	}

	private void drawArrow(Graphics gr, Point start, Point end) {
		Polygon triangle = new Polygon();
		double alpha = Math
				.atan((double) (start.y - end.y) / (start.x - end.x));
		if ((start.x - end.x) < 0) {
			alpha += Math.PI;
		}
		if (start.equals(end)) {
			alpha = 0;
		} 
		double headX = getEnd().x;
		double headY = getEnd().y;
		triangle.addPoint((int) Math.round(headX), (int) Math.round(headY));
		double beta = Math.atan((double) ARROW_WIGTH / ARROW_LENGTH);
		double x1 = headX + ARROW_SIDE * Math.cos(beta + alpha);
		double y1 = headY + ARROW_SIDE * Math.sin(beta + alpha);
		triangle.addPoint((int) Math.round(x1), (int) Math.round(y1));
		double x2 = headX + ARROW_SIDE * Math.cos(alpha - beta);
		double y2 = headY + ARROW_SIDE * Math.sin(alpha - beta);
		triangle.addPoint((int) Math.round(x2), (int) Math.round(y2));
		gr.drawPolygon(triangle);
		gr.fillPolygon(triangle);
	}

	private void drawWeight(Graphics gr, Point start, Point end,
			Color backgroundColor) {
		int x = (start.x + end.x) / 2;
		int y = (start.y + end.y) / 2;
		weightWidth = gr.getFontMetrics().stringWidth(
				Integer.toString(edge.getWeight()));
		weightHeight = gr.getFontMetrics().getMaxAscent()
				- gr.getFontMetrics().getMaxDescent();
		x -= (weightWidth - 1) / 2;
		y -= (weightHeight - 1) / 2;
		gr.setColor(backgroundColor);
		gr.fillRect(x - WEIGHT_SHIFT / 2, y - WEIGHT_SHIFT, weightWidth + WEIGHT_SHIFT, weightHeight + WEIGHT_SHIFT);
		gr.setColor(color);
		gr.drawString(Integer.toString(edge.getWeight()), x, y + weightHeight - 1);
	}

	/**
	 * Sets weight of this presentation.
	 * 
	 * @param weight
	 */
	public void setWeight(int weight) {
		edge.setWeight(weight);
		fireStructureChanged();
	}

	/**
	 * Returns this presentation weight.
	 * 
	 * @return weight
	 */
	public int getWeight() {
		return edge.getWeight();
	}

	/**
	 * Return upper left point of weight rect.
	 * 
	 * @return upper left point of weight rect
	 */
	public Point getUpperLeftWeightPoint() {
		if (!isWeighted) {
			return null;
		}
		int x = (getStart().x + getEnd().x) / 2;
		int y = (getStart().y + getEnd().y) / 2;
		return new Point(x - WEIGHT_SHIFT / 2, y - weightHeight / 2 - WEIGHT_SHIFT
				/ 2);
	}
	
	/**
	 * Marks this presentation.
	 * @param markColor 
	 *
	 */
	public void mark(Color markColor) {
		this.color = markColor;
	}
	
	/**
	 * Unmarks this presentation.
	 *
	 */
	public void unmark() {
		color = Color.black;
	}

	/**
	 * Verifies whether this presentation contains given point.
	 * 
	 * @param point
	 * @return true if it does; false otherwise
	 */
	public boolean containsPoint(Point point) {
		Point start = getStart();
		Point end = getEnd();
		int scalarProdWithStart = scalarProduct(point.x - start.x, point.y
				- start.y, end.x - start.x, end.y - start.y);
		int scalarProdWithEnd = scalarProduct(point.x - end.x, point.y - end.y,
				end.x - start.x, end.y - start.y);
		if ((scalarProdWithStart >= 0) && (scalarProdWithEnd <= 0)) {
			return (distanceToEdge(point, start, end) <= SELECTING_DISTANCE);

		}
		return false;
	}

	/**
	 * Verifies whether this presentation lays whithin giver rectangle
	 * 
	 * 
	 * @param x1 left upper x
	 * @param y1 left upper y
	 * @param x2 right lower x
	 * @param y2 right lower y
	 * @return true if it does; false otherwise
	 */
	public boolean isInRect(int x1, int y1, int x2, int y2) {
		return isPointInRect(getStart(), x1, y1, x2, y2)
				&& isPointInRect(getEnd(), x1, y1, x2, y2);
	}
	
	private boolean isPointInRect(Point point, int x1, int y1, int x2, int y2) {
		return (Math.min(x1, x2) <= point.x) && (Math.max(x1, x2) >= point.x) && (Math.min(y1, y2) <= point.y) && (Math.max(y1, y2) >= point.y);
	}

	/**
	 * Verifies whether given point is in this prenentation weight rect.
	 * 
	 * @param point
	 * @return true if it is; false otherwise
	 */
	public boolean isWeightSelected(Point point) {
		int x = (getStart().x + getEnd().x) / 2;
		int y = (getStart().y + getEnd().y) / 2;
		return (isWeighted && (x - WEIGHT_SHIFT / 2) <= point.x)
				&& (point.x <= (x + weightWidth + WEIGHT_SHIFT))
				&& ((y - weightHeight - WEIGHT_SHIFT / 2) <= point.y)
				&& (point.y <= (y + weightHeight + WEIGHT_SHIFT));
	}

	private int scalarProduct(int x1, int y1, int x2, int y2) {
		return x1 * x2 + y1 * y2;
	}

	private int distanceToEdge(Point point, Point p1, Point p2) {
		float result = (p1.y - p2.y) * point.x + (p2.x - p1.x) * point.y
				+ (p1.x * p2.y - p2.x * p1.y);
		result /= Math.sqrt((p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y)
				* (p1.y - p2.y));
		return Math.round(Math.abs(result));
	}

}
